﻿using System;
using System.Runtime.InteropServices;
using System.Text;

namespace TestWebApplication.CommercePaymentInstrument
{
    /// <summary>Implements a SCS compliant symmetric encryption algorithm.</summary>  
    public static class CommercePaymentInstrumentEncryptor
    {
        public static string Encrypt(string textToEncrypt, string publicKey)
        {
            publicKey = publicKey.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase) ?
                publicKey.Remove(0, 2) : publicKey;

            // Delete version string  
            string publicKeyMaterial = publicKey.Remove(0, 44);

            byte[] publicKeyBytes = HexToByteArray(publicKeyMaterial);

            int hProv = 0;

            if (CryptAcquireContext(ref hProv, IntPtr.Zero, null, PROV_RSA_AES, CRYPT_VERIFYCONTEXT) == 0)
            {
                throw new Exception(String.Format("Error aquiring key container context. ErrorId: {0}",
                    Marshal.GetLastWin32Error()));
            }

            int hPublicKey = 0;

            // Import public key to key container  
            if (CryptImportKey(hProv, publicKeyBytes, publicKeyBytes.Length, 0, CRYPT_EXPORTABLE,
                    ref hPublicKey) == 0)
            {
                throw new Exception(String.Format("Error importing key. ErrorId: {0}",
                    Marshal.GetLastWin32Error()));
            }

            int hSessionHandle = 0;

            if (CryptGenKey(hProv, new IntPtr(CALG_AES_192), CRYPT_EXPORTABLE,
                    ref hSessionHandle) == 0)
            {
                throw new Exception(String.Format("Error generating session key. ErrorId: {0}",
                    Marshal.GetLastWin32Error()));
            }

            int sessionBlobLength = 0;

            // exports length of wrapped session key  
            if (CryptExportKey(hSessionHandle, hPublicKey, SIMPLEBLOB, 0, IntPtr.Zero,
                    ref sessionBlobLength) == 0)
            {
                throw new Exception(String.Format("Error exporting session key length. ErrorId: {0}",
                    Marshal.GetLastWin32Error()));
            }

            byte[] exportedSessionKey = new byte[sessionBlobLength];

            // Exports session key wrapped with public key  
            if (CryptExportKey(hSessionHandle, hPublicKey, SIMPLEBLOB, 0, exportedSessionKey,
                    ref sessionBlobLength) == 0)
            {
                throw new Exception(String.Format("Error exporting session key. ErrorId: {0}",
                    Marshal.GetLastWin32Error()));
            }

            byte[] bytesToEncrypt = Encoding.Unicode.GetBytes(textToEncrypt);

            int dataLength = bytesToEncrypt.Length;
            int bufferLen = bytesToEncrypt.Length;

            // get the length of the cipher text  
            if (CryptEncrypt(hSessionHandle, 0, true, 0, null, ref dataLength, bufferLen) == 0)
            {
                throw new Exception(String.Format("Error while getting cipher text length. ErrorId: {0}",
                    Marshal.GetLastWin32Error()));
            }

            byte[] buffer = new byte[dataLength];
            Buffer.BlockCopy(bytesToEncrypt, 0, buffer, 0, bytesToEncrypt.Length);

            dataLength = bytesToEncrypt.Length;
            bufferLen = buffer.Length;

            if (CryptEncrypt(hSessionHandle, 0, true, 0, buffer, ref dataLength, bufferLen) == 0)
            {
                throw new Exception(String.Format("Error while encrypting data. ErrorId: {0}",
                    Marshal.GetLastWin32Error()));
            }

            // Key/container clean up  
            CryptDestroyKey(hSessionHandle);
            CryptDestroyKey(hPublicKey);
            CryptReleaseContext(hProv, 0);

            EncryptedData encryptedData = new EncryptedData();
            encryptedData.CipherText = buffer;
            encryptedData.ExportedSessionKey = exportedSessionKey;
            encryptedData.PublicKeyHeader = EncryptedData.ExtractPublicKeyHeader(publicKey);
            encryptedData.SessionKeyType = KeyType.SESSION_KEY_AES_192;

            return encryptedData.ToString();
        }

        public static byte[] EncryptToByteArray(string textToEncrypt, string publicKey)
        {
            return HexToByteArray(Encrypt(textToEncrypt, publicKey));
        }

        #region util functions

        private static byte[] HexToByteArray(String hexString)
        {
            int OutputByteCount = hexString.Length / 2;

            byte[] bytes = new byte[OutputByteCount];

            for (int i = 0; i < hexString.Length; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hexString.Substring(i, 2), 16);
            }

            return bytes;
        }

        #endregion

        #region CryptoAPI  CSP and key functions

        #region CryptoAPI Constants
        private const int CRYPT_VERIFYCONTEXT = -268435456;
        private const int PROV_RSA_FULL = 1;
        private const int PROV_RSA_AES = 24;
        private const int CRYPT_EXPORTABLE = 1;
        private const int SIMPLEBLOB = 1;
        private const int CALG_3DES = 26115; // (3 << 13) | (3 << 9) | 3;  
        private const int CALG_AES_192 = 26127; // (3 << 13) | (3 << 9) | 15;  
        #endregion CryptoAPI Constants

        #region CryptoApi functions
        [DllImport(@"advapi32.dll", SetLastError = true)] // do not remove SetLastError  
        private static extern int CryptAcquireContext(ref int phProv, IntPtr pszContainer,
            string pszProvider, int dwProvType, int dwFlags);

        [DllImport(@"advapi32.dll", SetLastError = true)]
        private static extern int CryptExportKey(int hKey, int hExpKey, int dwBlobType,
            int dwFlags, IntPtr pbData, ref int pdwDataLen);

        [DllImport(@"advapi32.dll", SetLastError = true)]
        private static extern int CryptExportKey(int hKey, int hExpKey, int dwBlobType,
            int dwFlags, byte[] pbData, ref int pdwDataLen);

        [DllImport(@"advapi32.dll", SetLastError = true)]
        private static extern int CryptImportKey(int hProv, byte[] pbData, int dwDataLen,
            int hPubKey, int dwFlags, ref int phKey);

        [DllImport(@"advapi32.dll")]
        private static extern int CryptEncrypt(int hKey, int hHash, bool Final, int dwFlags,
            byte[] pbData, ref int pdwDataLen, int dwBufLen);

        [DllImport(@"advapi32.dll", SetLastError = true)]
        private static extern int CryptGenKey(int hProv, IntPtr Algid, int dwFlags,
            ref int phKey);

        [DllImport(@"advapi32.dll", SetLastError = true)]
        private static extern int CryptDestroyKey(int hKey);

        [DllImport(@"advapi32.dll", SetLastError = true)]
        private static extern int CryptReleaseContext(int hProv, int dwFlags);
        #endregion CryptoApi functions

        #endregion CryptoAPI  CSP and key functions

        #region Structs/Enums

        private struct EncryptedData
        {
            public string PublicKeyHeader;
            public KeyType SessionKeyType;
            public byte[] ExportedSessionKey;
            public byte[] CipherText;

            public override String ToString()
            {
                byte[] tempBytes = new byte[4];

                StringBuilder outputBuilder = new StringBuilder();
                outputBuilder.Append(this.PublicKeyHeader);
                outputBuilder.Append("5400");   // 'T  

                Buffer.SetByte(tempBytes, 0, Convert.ToByte(4));    // sizeof(Int32)  
                outputBuilder.Append(ByteArrayToHex(tempBytes));

                Buffer.SetByte(tempBytes, 0, Convert.ToByte(this.SessionKeyType));
                outputBuilder.Append(ByteArrayToHex(tempBytes));

                Buffer.SetByte(tempBytes, 0, Convert.ToByte(this.ExportedSessionKey.Length));
                outputBuilder.Append(ByteArrayToHex(tempBytes));

                outputBuilder.Append(ByteArrayToHex(this.ExportedSessionKey));

                Buffer.SetByte(tempBytes, 0, Convert.ToByte(this.CipherText.Length));
                outputBuilder.Append(ByteArrayToHex(tempBytes));

                outputBuilder.Append(ByteArrayToHex(this.CipherText));

                return outputBuilder.ToString();
            }

            public static string ExtractPublicKeyHeader(string publicKey)
            {
                publicKey = publicKey.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase) ?
                    publicKey.Remove(0, 2) : publicKey;

                // extract the key header  
                return publicKey.Substring(0, 32);
            }

            private static string ByteArrayToHex(byte[] bytes)
            {
                char[] chars = new char[bytes.Length * 2];

                for (int i = 0; i < bytes.Length; i++)
                {
                    int b = bytes[i];
                    chars[i * 2] = hexDigits[b >> 4];
                    chars[i * 2 + 1] = hexDigits[b & 0xf];
                }

                return new string(chars);
            }

            private static char[] hexDigits = {  
                  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',  
                  'a', 'b', 'c', 'd', 'e', 'f'};
        }

        private enum KeyType : short
        {
            SESSION_KEY_3DES = 1,
            SESSION_KEY_AES_192 = 2
        }

        #endregion Structs/Enums
    }
}
